干货 | 高效开发与高性能并存的UI框架——携程Flutter实践
作者简介
段天章,携程支付中心Android端主力研发,目前主要负责中文版、国际版移动端Android支付模块研发工作。开源社区爱好者,热爱移动端新技术。
Flutter已经开源了三年,但是最近两年才开始在开源社区活跃起来,尤其是最近还发布了Preview 1版本。作为可以实现一套代码同时在iOS、Android平台上运行的又一个新的UI框架,Flutter提供给开发者的不仅仅是高速实现,还有高质量、流畅的UI。免费开源的协议对于开发者来说也很友好。
本文将从Flutter架构理念与UI渲染逻辑,来解释为什么Flutter的渲染效率非常高,以及从Flutter开发实践的角度,介绍框架的特性及Flutter开发中所遇到的问题,希望给对Flutter感兴趣的小伙伴在选型时一些启发和思考,避免重复踩坑。
一、Flutter Layers
Flutter的主要设计人之一Ian Hickson,之前是HTML规范编写者,因此Flutter的设计理念也与HTML的实现方法有很多相似之处。
Flutter最初的理念是实现跨平台的Material Design的跨平台框架。平台框架大致可以分为四层:
dart:ui : 最底层的是UI层,由Flutter引擎所暴露的库,可以理解为一个布局层。
Rendering : 这一层是抽象的布局层,它依赖于UI层,可以构建一个UI树,通过更新UI树来更新UI。
Material与Widgets : 最后就是Material层使用Widget层来构建UI。
起初Flutter是没有Rendering层的,直接通过坐标计算每个像素点需要显示什么,这让框架的代码变得特别复杂,每当UI更新的时候需要重新计算这些坐标是否需要改变。后来增加Randering层来抽象UI显示的位置,通过抽象位置来判断像素点是否需要更新。
在Flutter项目的初期,Dart-lang也不是特别成熟。Dart虚拟机在垃圾回收的频率与回收机制表现当时并不是特别好,比如当时Flutter如果运行一个时间很长的动画,动画结束之后所占用的内存对于Flutter框架就是一个很大的垃圾。后来Dart团队在垃圾回收上进行了很多优化,使Flutter在UI显示更流畅。
如今,国内最大的使用厂商应该就是阿里闲鱼了,在Flutter发布Preview 1版本的时候,闲鱼App也一起协同展示了他们用Flutter编写的商品详情页面。我也在使用Flutter仿小米计算器开发后,体验到release版的流畅度确实堪比原生:
(已上架Google,可以通过包名搜索下载体验:top.basking.calculator)
二、Flutter的UI渲染
Flutter渲染效率堪比原生,快于RN。Flutter更新UI的时候,并不是更新整个UI,而是更新所需要更新的部分。比如从网络异步下载一个图片,设置到“Image”(ImageView)中,如果这个Image Widget大小并没有改变,只需要将图片对象传入Widget中,接着直接重新绘制这一个Widget就可以了。为了达到这样的UI渲染理念,Flutter是如何设计的呢?
Flutter 的UI渲染过程简单可以分为3个分支,Widget树、Element树、Rendering树。
当Widget改变的时候,只有将它添加到Element树上时,才会改变Rendering树,展示到UI界面上。将它添加到Element树的方法就是setState()方法,它会自动寻找改变了的Widget,然后添加到Element树,等待后续的操作。
可以看到,矩形的子Widget并没有改变,所以在Element树上也没有改变,到了Rendering树也没有重新渲染,这种设计理念对于刷新UI操作可以大大提高效率。
与其他的UI框架渲染逻辑不同的是,Widget的Draw与Layout的顺序不一定相同。比如在Android端onDraw与onLayout的顺序是相同的。关于Flutter框架的渲染顺序大家可以看以下的例子:
在Row Widget中有三个子Widget,其中中间的是固定宽度的Widget,还有两个是根据剩下宽度比例占用位置的Widget,其中绿色Widget是橙色的宽度的两倍。而他们的layout order与rendering order如下:
这么做是因为Flutter为了保证对于每个Widget的访问是单一线性的。所以在layout order中Flutter框架就会先layout固定宽度的Widget,然后再layout比例宽度的Widget。接着到了Rendering树再会根据Element树的顺序逐个对每个Widget进行渲染。
三、Flutter框架UI特性
Flutter的开发语言是由ChromeV8引擎团队的领导者Lars Bak主持开发的Dart。Dart语言语法类似于C。Dart语言为了更好的适应FlutterUI框架,在内存分配和垃圾回收做了很多优化。
因为Dart在连续分配多个对象的时候,所需消耗的资源非常少。Dart虚拟机可以快速分配内存给短期生存的对象,这样可以使很复杂的UI在60ms内完成一帧的渲染(实际感觉每一帧渲染时间更短),这样就保证了Flutter可以平滑的展示UI滑动及动画等效果。Flutter团队与Dart团队的密切合作让提升效率变得更加容易。
Flutter在开发UI界面的时候,又比较像HTML的标签式语言,前文也提到,这是受Flutter创始人之一的Ian Hickson影响。其实很多UI布局都是类似标签的样式来编写的,比如Android的XML以及网页的HTML,所以Flutter会采用这样一个成熟的布局开发样式。
new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
new FlatButton(
color: Colors.blue,
)
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
Flutter与RN一样,在原生开发中很依赖于插件来调用系统API,毕竟它是一个UI框架。但是现阶段的Flutter插件并不是像RN那么全,可以看到维护Flutter的开发者只有200多人,而维护react-native的开发者已经近1700人了,一个数量级之差的维护者肯定在插件数量与开发体验上差别很大。
在包管理上,flutter并不需要依赖第三方类似于RN的npm包管理器来添加依赖,flutter本身就自带了包管理器,只需要在pubspec.yaml文件中添加相关依赖即可。但是,因为Google的库在国不能访问,需要添加环境变量指定库镜像才可以使用。
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
在代码实现上,Flutter并没有Android的findViewById,页面布局是通过有状态Widget(StatefulWidget)和无状态Widget(StatelessWidget)实现的。顾名思义,无状态的Widget就是一些不可以改变的UI,而需要改变的UI则是通过有状态的Widget来实现,并且通过setStatus()来刷新UI的状态:
...
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
...
setState(() {
_counter--;
});
这种方法很简单的实现了动态化的UI及Android长久以来希望达到的目标 —— data binding。
四、Flutter待完善的方面及使用中遇到的问题
Dart并不是没有反射,dart:mirrors就具有Mirror概念的反射。在安全、分发、部署方面,Mirror-Base具有很大优势。但是反射生成的代码冗长,会使Flutter编译过后的包很大。Flutter通过将Dart编译成原生代码本身就会增加包大小,再加上反射的话包大小更会进一步扩大。所以Flutter团队在现阶段并没有开放dart:mirrors的使用。
没有反射也就意味着Json String to Model 也没有办法完成,对于这一点,官方也比较无奈。至今Flutter中Dart只支持将JsonString 转化为Map,然后再由开发者手写代码将key值一一对应到相应的字段上。
/**
"result": {
"status": "ALREADY",
"scur": "CNY",
"tcur": "EUR",
"ratenm": "人民币/欧元",
"rate": "0.127839",
"update": "2018-07-13 23:28:01"
}
*/
///
class ExchangeResult {
final String status;
final String scur;
final String tcur;
final String ratem;
final String rate;
final String update;
ExchangeResult(this.status, this.scur, this.tcur, this.ratem, this.rate,
this.update,);
ExchangeResult.fromJson(Map<String, dynamic> json)
: status = json['status'],
scur = json['scur'],
tcur = json['tcur'],
ratem = json['ratem'],
rate = json['rate'],
update = json['update'];
}
Map exchangeMap = json.decode(Utf8Codec().decode(response.bodyBytes));
var resultModel = new ExchangeResult.fromJson(exchangeMap);
Http请求返回的response中Header会包含编码格式charset=utf-8,官方给出的Demo如下:
var dataURL = "http://api.k780.com?app=finance.rate&scur=CNY&tcur=GBP&appkey=35134&sign=fb020c3129435bb5ff21b7113e9cb1c1&format=json";
var response = await http.get(dataURL);
print(response.body);
看起来是非常简单的实现了异步请求服务,但是如果返回的charset后面多加了一个";"的话 (charset=utf-8;),http client就不会自动根据header中的charset解析,会返回错误:
[ERROR:topaz/lib/tonic/logging/dart_error.cc(16)] Unhandled exception:
Error on line 1, column 33: Invalid media type: expected
所以,如果要解析返回的json string,必须要指定UTF8字符解析response才可以:
print(Utf8Codec().decode(response.bodyBytes));
安装Flutter的同时也会安装Dart lang SDK,集成在Flutter的SDK中的$FLUTTER_SDK/bin/cache/dart-sdk。假如你发现一个Dart lang bug,那就需要更改DartSDK的代码,但是这个修正并不能让你马上使用。因为Flutter与Dart lang SDK 的version是一一绑定好的。
五、总结
Flutter虽然在现阶段问题比较多,但是相对于RN也有自身的优势。
在性能方面,Flutter的表现比RN更为优秀。Flutter也可以与原生混编,不过Flutter项目在编译过后生成的安装包相对于原生开发的项目来说会有所增大,相信这是Flutter团队今后要解决的一大难题。
不过随着google与开源社区的不断支持,相信Flutter在跨平台移动应用开发中将成为一种新趋势。
【推荐阅读】